﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ExpressionTreeVisitors;
using Remotion.Linq.Parsing;

namespace Gumini.JulaDB.Linq
{
    public class JulaExpressionTreeVisitor : ThrowingExpressionTreeVisitor
    {
        private readonly Dictionary<ExpressionType, string> _operatorMapping = new Dictionary<ExpressionType, string>()
        {
            { ExpressionType.Equal, " = "},
            { ExpressionType.NotEqual, " != "},
            { ExpressionType.And, " and "},
            { ExpressionType.AndAlso, " and "},
            { ExpressionType.Or, " or "},
            { ExpressionType.OrElse, " or "},
            { ExpressionType.Add, " + "},
            { ExpressionType.Subtract, " - "},
            { ExpressionType.Multiply, " * "},
            { ExpressionType.Divide, " / "},
            { ExpressionType.GreaterThan, " > "},
            { ExpressionType.GreaterThanOrEqual, " >= "},
            { ExpressionType.LessThan, " < "},
            { ExpressionType.LessThanOrEqual, " <= "}
        };
        private readonly StringBuilder _julaExpression = new StringBuilder();

        private JulaExpressionTreeVisitor()
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::ctor");
        }
        
        // Called when a LINQ expression type is not handled above.
        protected override Exception CreateUnhandledItemException<T>(T unhandledItem, string visitMethod)
        {
            string itemText = FormatUnhandledItem(unhandledItem);
            var message = string.Format("The expression '{0}' (type: {1}, method: {2}) is not supported by this LINQ provider.",
                itemText, typeof(T), visitMethod);
            return new NotSupportedException(message);
        }

        private string FormatUnhandledItem<T>(T unhandledItem)
        {
            var itemAsExpression = unhandledItem as Expression;
            return itemAsExpression != null ? FormattingExpressionTreeVisitor.Format(itemAsExpression) : unhandledItem.ToString();
        }

        public static string GetExpressionString(Expression linqExpression)
        {
            var visitor = new JulaExpressionTreeVisitor();
            visitor.VisitExpression(linqExpression);
            return visitor.GetExpressionString();
        }

        public string GetExpressionString()
        {
          return _julaExpression.ToString();
        }

        protected override Expression VisitNewExpression(NewExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitNewExpression");
            var expressionStrings = expression.Arguments.Select(ex => GetExpressionString(ex));
            _julaExpression.Append(String.Join(", ", expressionStrings));
            return expression;
        }

        protected override Expression VisitQuerySourceReferenceExpression(QuerySourceReferenceExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitQuerySourceReferenceExpression");
            var itemType = expression.ReferencedQuerySource.ItemType;
            var selectionList = new List<string>();
            var properties = itemType.GetProperties();
            if(properties.Length == 0)
                throw new NotSupportedException(String.Format("Type {0} has no properties.", itemType));
            foreach(var property in properties)
            {
                selectionList.Add(String.Format("{0}.{1}",
                    expression.ReferencedQuerySource.ItemName, property.Name));
            }
            _julaExpression.Append(string.Join(", ", selectionList));
            return expression;
        }

        protected override Expression VisitMemberExpression(MemberExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitMemberExpression");
            string source = ((QuerySourceReferenceExpression)expression.Expression).
                ReferencedQuerySource.ItemName;
            _julaExpression.AppendFormat("{0}.{1}", source, expression.Member.Name);
            return expression;
        }

        protected override Expression VisitBinaryExpression(BinaryExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitBinaryExpression");

            // we will throw an error for unsupported operators
            if (!_operatorMapping.ContainsKey(expression.NodeType))
                return base.VisitBinaryExpression(expression);

            _julaExpression.Append ("(");
            VisitExpression(expression.Left);

            // special case for string concatenation
            switch (expression.NodeType)
            {
                case ExpressionType.Add:
                    if (expression.Left.Type == typeof(string) && expression.Right.Type == typeof(string))
                        _julaExpression.Append(" || ");
                    break;
                case ExpressionType.Equal:
                    if (expression.Right.ToString() == "null")
                        _julaExpression.Append(" is ");
                    break;
                case ExpressionType.NotEqual:
                    if (expression.Right.ToString() == "null")
                        _julaExpression.Append(" is not ");
                    break;
                default:
                    _julaExpression.Append(_operatorMapping[expression.NodeType]);
                    break;
            }
            
            VisitExpression(expression.Right);
            _julaExpression.Append (")");

            return expression;
        }

        protected override Expression VisitUnaryExpression(UnaryExpression expression)
        {
            if (expression.NodeType == ExpressionType.Not)
            {
                _julaExpression.Append("NOT (");
                VisitExpression(expression.Operand);
                _julaExpression.Append(")");
                return expression;
            }
            return base.VisitUnaryExpression(expression);
        }

        protected override Expression VisitConstantExpression(ConstantExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitConstantExpression");

            if (expression.Value == null)
            {
                _julaExpression.Append("null");
            }
            if (expression.Type == typeof(string))
            {
                _julaExpression.AppendFormat("'{0}'", expression.Value);
            }
            else if (Utils.IsNumericType(expression.Type))
            {
                _julaExpression.Append(String.Format(CultureInfo.InvariantCulture, "{0}", expression.Value));
            }
            else
                base.VisitConstantExpression(expression);
            return expression;
        }

        protected override Expression VisitSubQueryExpression(SubQueryExpression expression)
        {
            Debug.WriteLine("JulaExpressionTreeVisitor::VisitSubQueryExpression");
            return base.VisitSubQueryExpression(expression);
        }

        private void BuildUpLikeClause(Expression arg1, Expression arg2, bool startsWith, bool endsWith)
        {
            _julaExpression.Append("(");
            VisitExpression(arg1);
            _julaExpression.Append(" like (");
            if (!startsWith)
                _julaExpression.Append("'%'||");
            VisitExpression(arg2);
            if (!endsWith)
                _julaExpression.Append("||'%'");
            _julaExpression.Append("))");
        }

        protected override Expression VisitMethodCallExpression(MethodCallExpression expression)
        {
            if (expression.Method.Equals(typeof(string).GetMethod("Contains", new Type[] { typeof(string) })))
            {
                BuildUpLikeClause(expression.Object, expression.Arguments[0],
                    startsWith: false, endsWith: false);
            }
            else if (expression.Method.Equals(typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) })))
            {
                BuildUpLikeClause(expression.Object, expression.Arguments[0],
                    startsWith: true, endsWith: false);
            }
            else if (expression.Method.Equals(typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) })))
            {
                BuildUpLikeClause(expression.Object, expression.Arguments[0],
                    startsWith: false, endsWith: true);
            }
            else
                return base.VisitMethodCallExpression(expression); // throws an exception
            return expression;
        }
    }
}
